<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Nami WebSocket客户端 - 修复版</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }

        body {
            background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }

        .container {
            width: 100%;
            max-width: 800px;
            background: rgba(255, 255, 255, 0.92);
            border-radius: 12px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
            overflow: hidden;
        }

        header {
            background: #2c3e50;
            color: white;
            padding: 20px;
            text-align: center;
            position: relative;
        }

        header h1 {
            font-size: 24px;
            margin-bottom: 5px;
        }

        .status-bar {
            display: flex;
            justify-content: space-between;
            background: #34495e;
            color: white;
            padding: 10px 20px;
            font-size: 14px;
        }

        .status-indicator {
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .indicator {
            width: 12px;
            height: 12px;
            border-radius: 50%;
            background: #e74c3c;
        }

        .indicator.connected {
            background: #2ecc71;
        }

        .form-container {
            padding: 25px;
        }

        .form-group {
            margin-bottom: 20px;
        }

        label {
            display: block;
            margin-bottom: 8px;
            font-weight: 600;
            color: #2c3e50;
        }

        input, select, textarea {
            width: 100%;
            padding: 12px 15px;
            border: 1px solid #d1d1d1;
            border-radius: 6px;
            font-size: 15px;
            background: #f8f9fa;
            transition: all 0.3s;
        }

        input:focus, select:focus, textarea:focus {
            border-color: #3498db;
            box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
            outline: none;
            background: #fff;
        }

        .button-group {
            display: flex;
            gap: 15px;
            margin: 25px 0;
            flex-wrap: wrap;
        }

        .btn {
            padding: 12px 25px;
            border: none;
            border-radius: 6px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s;
            flex: 1;
            min-width: 120px;
            font-size: 15px;
        }

        .btn-primary {
            background: linear-gradient(to right, #3498db, #2980b9);
            color: white;
        }

        .btn-primary:hover {
            background: linear-gradient(to right, #2980b9, #2573a7);
            transform: translateY(-2px);
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
        }

        .btn-secondary {
            background: linear-gradient(to right, #27ae60, #2ecc71);
            color: white;
        }

        .btn-secondary:hover {
            background: linear-gradient(to right, #2ecc71, #27ae60);
            transform: translateY(-2px);
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
        }

        .btn-danger {
            background: linear-gradient(to right, #e74c3c, #c0392b);
            color: white;
        }

        .btn-danger:hover {
            background: linear-gradient(to right, #c0392b, #a93226);
            transform: translateY(-2px);
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
        }

        .output-container {
            margin-top: 20px;
            background: #f8f9fa;
            border-radius: 8px;
            padding: 20px;
            border: 1px solid #e9ecef;
        }

        .output-container h3 {
            margin-bottom: 15px;
            color: #2c3e50;
            padding-bottom: 10px;
            border-bottom: 1px solid #e9ecef;
        }

        textarea {
            width: 100%;
            height: 200px;
            padding: 15px;
            border: 1px solid #ced4da;
            border-radius: 6px;
            font-family: monospace;
            font-size: 14px;
            background: #2d3748;
            color: #e2e8f0;
            resize: vertical;
        }

        .instructions {
            background: #e3f2fd;
            border-left: 4px solid #2196f3;
            padding: 15px;
            border-radius: 0 8px 8px 0;
            margin: 20px 0;
        }

        .instructions h3 {
            margin-bottom: 10px;
            color: #0d47a1;
        }

        @media (max-width: 600px) {
            .button-group {
                flex-direction: column;
            }

            .container {
                margin: 10px;
            }

            .form-container {
                padding: 15px;
            }
        }
    </style>
</head>
<body>
<div class="container">
    <header>
        <h1>Nami WebSocket 客户端</h1>
        <p>修复重连功能并优化用户体验</p>
    </header>

    <div class="status-bar">
        <div class="status-indicator">
            <div class="indicator" id="statusIndicator"></div>
            <span id="connectionStatus">未连接</span>
        </div>
        <div id="lastHeartbeat">最后心跳: 从未</div>
    </div>

    <div class="form-container">
        <div class="instructions">
            <h3>使用说明</h3>
            <p>首先输入WebSocket地址、用户ID、App Key和系统信息，然后点击"登录"建立连接。连接后可以发送单聊或群聊消息。</p>
        </div>

        <form id="wsForm">
            <div class="form-group">
                <label for="wsUrl">WebSocket 地址</label>
                <input type="text" id="wsUrl" value="ws://localhost:30000/ws" placeholder="ws://example.com:port/path">
            </div>

            <div class="form-group">
                <label for="userId">用户 ID</label>
                <input type="text" id="userId" placeholder="输入用户ID" value="1">
            </div>

            <div class="form-group">
                <label for="appKey">App Key</label>
                <input type="text" id="appKey" placeholder="输入App Key" value="test">
            </div>

            <div class="form-group">
                <label for="platform">Platform</label>
                <input type="text" id="platform" placeholder="输入平台" value="Win">
            </div>

            <div class="button-group">
                <button type="button" class="btn btn-primary" id="loginBtn">登录</button>
                <button type="button" class="btn btn-danger" id="disconnectBtn">断开连接</button>
            </div>

            <div class="form-group">
                <label for="msgType">消息类型</label>
                <select id="msgType">
                    <option value="p_msg">单聊</option>
                    <option value="g_msg">群聊</option>
                </select>
            </div>

            <div class="form-group">
                <label for="receiveId">接收者 ID</label>
                <input type="text" id="receiveId" placeholder="输入接收者ID">
            </div>

            <div class="form-group">
                <label for="message">消息内容</label>
                <textarea id="message" placeholder="输入要发送的消息内容"></textarea>
            </div>

            <div class="button-group">
                <button type="button" class="btn btn-secondary" id="sendBtn">发送消息</button>
                <button type="button" class="btn btn-danger" id="clearBtn">清空输出</button>
            </div>
        </form>

        <div class="output-container">
            <h3>服务器响应</h3>
            <textarea id="responseText" readonly></textarea>
        </div>

        <div class="button-group" style="margin-top: 20px;">
            <button type="button" class="btn btn-secondary" id="reconnectBtn">重新连接</button>
        </div>
    </div>
</div>

<!-- 加载依赖库 -->
<script src="js/google-protobuf.js"></script>
<script src="js/goog-base.js"></script>
<script src="js/protobuf/connect/connectackmsgbody.js"></script>
<script src="js/protobuf/connect/publishackmsgbody.js"></script>
<script src="js/protobuf/downmsg/downmsgbody.js"></script>
<script src="js/protobuf/connect/connectmsgbody.js"></script>
<script src="js/protobuf/connect/publishmsg.js"></script>
<script src="js/protobuf/connect/simplifieddownmsg.js"></script>
<script src="js/protobuf/immessages/upmsg.js"></script>
<script src="js/protobuf/immessages/downmsg.js"></script>
<script src="js/protobuf/immessages/channeltype.js"></script>

<!--<script type="module" src="js/enum.js" ></script>-->
<!--<script type="module" src="js/provider/session/index.js" ></script>-->
<!--<script type="module" src="js/provider/session/uni.js"></script>-->
<!--<script type="module" src="js/provider/session/web.js"></script>-->

<script type="text/javascript">

    var socket = null;
    var qosQueue = [];
    var isQosRunning = false;
    var heartBeatTimer = null;
    var qosTimer = null;
    var isConnected = false;

    // DOM元素
    const statusIndicator = document.getElementById('statusIndicator');
    const connectionStatus = document.getElementById('connectionStatus');
    const lastHeartbeat = document.getElementById('lastHeartbeat');
    const responseText = document.getElementById('responseText');
    const reconnectBtn = document.getElementById('reconnectBtn');

    // 全局重连函数 - 修复了未定义错误
    function reconnectSocket() {
        addToOutput('正在尝试重新连接...');
        updateConnectionStatus(false, '连接中...');

        // 如果已有连接，先关闭
        if (socket && socket.readyState === WebSocket.OPEN) {
            socket.close();
        }

        // 清除定时器
        if (heartBeatTimer) clearTimeout(heartBeatTimer);
        if (qosTimer) clearInterval(qosTimer);

        // 重新初始化连接
        setTimeout(viewsocket, 500);
    }

    // 断开连接函数
    function disconnectSocket() {
        if (socket) {
            if (socket.readyState === WebSocket.OPEN) {
                socket.close();
                addToOutput('连接已手动断开');
            }

            // 清除定时器
            clearTimeout(heartBeatTimer);
            clearInterval(qosTimer);
        }

        updateConnectionStatus(false, '未连接');
    }

    // 更新连接状态显示
    function updateConnectionStatus(connected, message) {
        isConnected = connected;
        connectionStatus.textContent = message;
        statusIndicator.className = connected ? 'indicator connected' : 'indicator';
    }

    // 添加输出到文本区域
    function addToOutput(message) {
        responseText.value += message + '\n';
        responseText.scrollTop = responseText.scrollHeight;
    }

    // 定义 createCustomMessage 函数
    function sendCommand(cmd, version, qos, dataView) {
        // 魔术值固定为8
        const magic = 8;

        // 假设所有字节数据都在0-255之间

        // 创建魔术值视图
        const magicView = new DataView(new ArrayBuffer(1));
        magicView.setUint8(0, magic);

        const versionView = new DataView(new ArrayBuffer(1));
        versionView.setUint8(0, version);

        // 创建cmd视图，现在它是4个字节
        const cmdView = new DataView(new ArrayBuffer(1));
        cmdView.setUint8(0, cmd, false); // 注意endianness（这里是大端序）

        const qosView = new DataView(new ArrayBuffer(1));
        qosView.setUint8(0, qos);

        // 将字符串转换为UTF-8编码的ArrayBuffer
        // let encoder = new TextEncoder();
        // let dataView = encoder.encode(dataStr);

        // 计算并添加4字节的长度字段
        let dataLength = dataView.byteLength;
        console.log("[sendCommand] datalenght=", dataLength)
        const lengthView = new DataView(new ArrayBuffer(4));
        lengthView.setUint32(0, dataLength, false); // 注意endianness（这里是大端序）

        // 合并所有的ArrayBuffer
        let totalLength = 1 + 1 + 1 + 1 + 4 + dataView.byteLength;
        let combinedBuffer = new ArrayBuffer(totalLength);
        let combinedView = new DataView(combinedBuffer);

        // 复制每个部分到combinedBuffer中
        let offset = 0;

        // 首先复制魔术值
        combinedView.setUint8(offset++, magicView.getUint8(0));
        combinedView.setUint8(offset++, versionView.getUint8(0));

        combinedView.setUint8(offset++, cmdView.getUint8(0));

        combinedView.setUint8(offset++, qosView.getUint8(0));

        // 复制长度字段
        for (let i = 0; i < 4; i++) {
            combinedView.setUint8(offset++, lengthView.getUint8(i));
        }

        // 最后复制数据
        for (let i = 0; i < dataView.byteLength; i++) {
            combinedView.setUint8(offset++, dataView[i]);
        }

        return combinedBuffer;
    }

    function getType(value) {
        if (value === null) return 'null';
        if (Array.isArray(value)) return 'array';
        return typeof value;
    }

    // 解析二进制数据
    function parseBinaryDataToObject(event) {
        const combinedView = new DataView(event.data);
        const magic = combinedView.getUint8(0);
        const version = combinedView.getUint8(1);
        const cmd = combinedView.getUint8(2);
        const qos = combinedView.getUint8(3);

        const length = combinedView.getUint32(4);
        const dataOffset = 8;
        // 提取body数据
        const bodyBuffer = event.data.slice(dataOffset, dataOffset + length);

        // const decoder = new TextDecoder();
        // const dataStr = decoder.decode(dataBuffer);
        if (cmd === 2) {
            let downMsgBody = proto.ConnectAckMsgBody.deserializeBinary(new Uint8Array(bodyBuffer));
            console.log("解析下行登录消息 :", downMsgBody.getCode(), downMsgBody.getExt())
            return {version, cmd, qos, data: downMsgBody};
        }

        if (cmd === 3) {
            let publishBody = proto.PublishMsg.deserializeBinary(new Uint8Array(bodyBuffer));
            console.log("收到publish消息，topic: ", publishBody.getTopic(), publishBody.getTargetid());
            if (publishBody.getTopic() === 'msg') {
                let downMsg = proto.DownMsg.deserializeBinary(publishBody.getData())
                console.log("downmsg:", downMsg)
                // const decoder = new TextDecoder();
                // const body  = decoder.decode(publishBody.getData());
                // console.log("data :", body)
                // let data = JSON.parse(body);
                // console.log("publish消息详细data: ", data);
                let wrapper = {
                    "targetId": publishBody.getTargetid(),
                    "msgContent": downMsg.msgContent,
                    "msgType": downMsg.msgType,
                    "clientUid": downMsg.clientUid,
                    "msgId": downMsg.msgId,
                    "index": publishBody.getIndex()
                }
                return {version, cmd, qos, data: downMsg}
            } else if (publishBody.getTopic() === 'sync_msg') {
                let downMsg = proto.DownMsg.deserializeBinary(publishBody.getData())
                // const decoder = new TextDecoder();
                // const body = decoder.decode(publishBody.getData());
                // let data = JSON.parse(body);
                // console.log("publish消息详细data: ", data);
                // let wrapper = {
                //     "targetId": data.getTargetid(),
                //     "msgContent": data.msgContent,
                //     "msgType": data.msgType,
                //     "clientUid": data.clientUid,
                //     "msgId": data.msgId,
                //     "index": publishBody.getIndex()
                // }
                return {version, cmd, qos, data: downMsg}
            }

        }
        if (cmd === 4) {
            let downMsgBody = proto.PublishAckMsgBody.deserializeBinary(new Uint8Array(bodyBuffer));
            console.log("解析下行puback消息：" + downMsgBody.getCode() + " , id=" + downMsgBody.getMsgid())
            return {version, cmd, qos, data: downMsgBody}
        }

        if (cmd === 13) {
            return {version, cmd, qos, data: null};
        }

        console.log("----------------------> cmd:", cmd)

        // return { version, cmd, qos, data: dataStr };
    }

    // 登录函数
    function login() {
        const userId = document.getElementById('userId').value;
        const platform = document.getElementById('platform').value;
        const appKey = document.getElementById('appKey').value;

        if (!userId || !appKey) {
            addToOutput('错误：用户ID和App Key不能为空');
            return;
        }

        if (socket && socket.readyState === WebSocket.OPEN) {
            const connectMsgBody = new proto.ConnectMsgBody();
            connectMsgBody.setUserid(userId);
            connectMsgBody.setAppkey(appKey);
            connectMsgBody.setSdkversion("0.0.1");
            connectMsgBody.setLanguage("Chinese");
            connectMsgBody.setProtoid("1");
            connectMsgBody.setDeviceid("123");
            connectMsgBody.setClientip("127.0.0.1");
            connectMsgBody.setInstanceid(getClientSession());

            connectMsgBody.setPlatform(platform);
            connectMsgBody.setToken("Bearer: xxxxxxxxx");

            connectMsgBody.setTimestamp(Date.now());

            const serialConnectMsg = connectMsgBody.serializeBinary();
            socket.send(sendCommand(1, 1, 1, serialConnectMsg));
            addToOutput('登录请求已发送');
        } else {
            addToOutput('错误：连接未建立，请先连接服务器');
        }
    }

    // 发送消息函数
    function sendMsg() {
        const userId = document.getElementById('userId').value;
        const receiveId = document.getElementById('receiveId').value;
        const msgType = document.getElementById('msgType').value;
        const msg = document.getElementById('message').value;

        if (!userId || !receiveId || !msg) {
            addToOutput('错误：用户ID、接收者ID和消息内容不能为空');
            return;
        }

        if (!socket || socket.readyState !== WebSocket.OPEN) {
            addToOutput('错误：连接未建立');
            return;
        }

        // const messageData = {
        //     clientUid: uuid(),
        //     msgContent: `{"content": "${msg}"}`,
        //     msgType: "text"
        // };
        //
        // const jsonData = JSON.stringify(messageData);
        // // 将 JSON 字符串转换为 Uint8Array
        // const encoder = new TextEncoder();
        // const dataAsUint8Array = encoder.encode(jsonData);
        var upMsg = new proto.UpMsg();
        upMsg.setClientuid(uuid())
        let msgContent = `{"content": "${msg}"}`;
        var msgContentBytes = new TextEncoder().encode(msgContent);
        upMsg.setMsgcontent(msgContentBytes)
        upMsg.setMsgtime(Date.now())
        upMsg.setFlags(10)
        upMsg.setMsgtype("text")

        let publishBody = new proto.PublishMsg();
        publishBody.setTopic("upmsg-" + msgType);
        publishBody.setTargetid(receiveId);
        publishBody.setData(upMsg.serializeBinary());

        socket.send(sendCommand(3, 1, 1, publishBody.serializeBinary()));
        addToOutput(`已发送消息: ${msg}`);
    }

    // 查看套接字函数
    function viewsocket() {
        const wsUrl = document.getElementById("wsUrl").value;
        if (!("WebSocket" in window)) {
            addToOutput('错误：您的浏览器不支持WebSocket');
            return;
        }

        socket = new WebSocket(wsUrl);
        socket.binaryType = "arraybuffer";
        let userId = document.getElementById('userId').value;
        socket.onopen = function (event) {
            updateConnectionStatus(true, '已连接');
            addToOutput('连接已开启');

            // 设置QoS定时器
            if (qosTimer) clearInterval(qosTimer);
            qosTimer = setInterval(sendBatchQos, 1000);

            // // 开始心跳
            // heartBeat();
        };

        socket.onmessage = function (event) {
            const receivedObject = parseBinaryDataToObject(event);
            const cmd = receivedObject.cmd;

            if (cmd === 2) { // 登录响应
                let resp = receivedObject.data;
                if (resp.getCode() === "200") {
                    addToOutput('登录成功!');
                    heartBeat(); // 开始心跳
                } else {
                    let msg = "参数错误！";
                    if (resp.getCode() === "1002") {
                        msg = "当前客户端已登录，不允许重复登录!";
                    }
                    addToOutput('登录失败: ' + msg);
                }
                heartBeat();
            } else if (cmd === 13) { // 心跳响应
                console.log("收到心跳 time:", new Date().toLocaleTimeString());
                lastHeartbeat.textContent = `最后心跳: ${new Date().toLocaleTimeString()}`;
                heartBeat(); // 设置下一次心跳
            } else if (cmd === 4) { // puback
                let resp = receivedObject.data;
                addToOutput(`收到来自服务器ack code: ${resp.getCode()} msgId:${resp.getMsgid()} clientMsgId: ${resp.getClientmsgid()} `);
            } else if (cmd === 3) { // publish 消息
                var data = receivedObject.data;
                console.log("cmd=3 :", data)
                let decoderStr = new TextDecoder().decode(data.getMsgcontent_asU8());
                let messageStr = JSON.parse(decoderStr);

                if (data.getChanneltype() === proto.ChannelType.PRIVATE) {
                    if (data.targetId != userId) {
                        addToOutput(`收到来自 ${data.getTargetid()} 的单聊消息: ${messageStr.content}`);
                        qosQueue.push({
                            msgId: data.getMsgid(),
                            converType: 'c2c' // 单聊
                        });
                    }
                }
                if(data.getChanneltype() === proto.ChannelType.GROUP) {
                    if (data.targetId != userId) {
                        addToOutput(`收到来自 ${data.getTargetid()} 的群聊消息: ${messageStr.content}`);
                        qosQueue.push({
                            msgId: data.getMsgid(),
                            converType: 'c2g' //
                        });
                    }
                }

            } else if (cmd === 1005) { // 群聊消息响应
                const bodyJson = JSON.parse(receivedObject.data);
                const messageJson = JSON.parse(bodyJson.data);
                addToOutput(`收到来自 ${messageJson.senderId} 的群聊消息: ${messageJson.payload}`);
            }
        };

        socket.onclose = function (event) {
            updateConnectionStatus(false, '未连接');
            addToOutput('连接已关闭');

            // 清除定时器
            if (heartBeatTimer) clearTimeout(heartBeatTimer);
            if (qosTimer) clearInterval(qosTimer);
        };

        socket.onerror = function (error) {
            updateConnectionStatus(false, '连接错误');
            addToOutput('连接错误: ' + JSON.stringify(error));
        };
    }

    // 心跳函数
    function heartBeat() {
        if (!socket || socket.readyState !== WebSocket.OPEN) return;

        heartBeatTimer = setTimeout(() => {
            let data = new DataView(new ArrayBuffer(0));
            socket.send(sendCommand(12, 1, 0, data));
            addToOutput('发送心跳...');
            heartBeatTimer = null;
        }, 30000); // 55秒
    }

    // 批量QoS函数
    function sendBatchQos() {
        if (isQosRunning || qosQueue.length === 0) return;

        isQosRunning = true;
        const groupedQos = {};
        qosQueue.forEach(item => console.log(" qos msgId=", item.msgId));
        qosQueue.forEach(item => {
            if (!groupedQos[item.converType]) groupedQos[item.converType] = [];
            groupedQos[item.converType].push(item.msgId);
        });

        Object.keys(groupedQos).forEach(converType => {
            // const userId = document.getElementById('userId').value || '0';
            // const qosData = {
            //     msgIds: groupedQos[converType],
            // };
            var simplifiedDownMsg = new proto.SimplifiedDownMsg();
            simplifiedDownMsg.setMsgcontent(new TextEncoder().encode(converType));
            var publishAckMsgBody = new proto.PublishAckMsgBody();
            publishAckMsgBody.setMsgid(groupedQos[converType].join(","));
            publishAckMsgBody.setModifiedmsg(simplifiedDownMsg)
            publishAckMsgBody.setTimestamp(Date.now())
            console.log("-----发送qos  msgId:", groupedQos[converType]);
            socket.send(sendCommand(4, 1, 1, publishAckMsgBody.serializeBinary()));
        });

        qosQueue = [];
        isQosRunning = false;
    }


    function genUId() {
        return 'xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
            let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
            return v.toString(16);
        });
    }

    function getClientSession() {
        let clientSession = sessionStorage.getItem("jgim_client_session");
        if (!clientSession) {
            clientSession = genUId();
            sessionStorage.setItem("jgim_client_session", clientSession);
        }
        return clientSession;
    }

    // UUID生成函数
    function uuid() {
        const hexDigits = "0123456789abcdef";
        const s = [];

        for (let i = 0; i < 36; i++) {
            s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
        }

        s[14] = "4";
        s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
        s[8] = s[13] = s[18] = s[23] = "-";

        return s.join("");
    }

    // 清空输出
    function clearOutput() {
        responseText.value = '';
    }

    // 添加事件监听器
    document.getElementById('loginBtn').addEventListener('click', login);
    document.getElementById('disconnectBtn').addEventListener('click', disconnectSocket);
    document.getElementById('sendBtn').addEventListener('click', sendMsg);
    document.getElementById('clearBtn').addEventListener('click', clearOutput);
    reconnectBtn.addEventListener('click', reconnectSocket);

    // 初始化连接
    viewsocket();
</script>
</body>
</html>
